<?php

// ------------------------------------------------------------------------
// |@Author       : Jarmin <topextend@qq.com>
// |@----------------------------------------------------------------------
// |@Date         : 2025-05-07 17:17:33
// |@----------------------------------------------------------------------
// |@LastEditTime : 2025-05-09 18:25:27
// |@----------------------------------------------------------------------
// |@LastEditors  : Jarmin <topextend@qq.com>
// |@----------------------------------------------------------------------
// |@Description  :
// |@----------------------------------------------------------------------
// |@FilePath     : JwtService.php
// |@----------------------------------------------------------------------
// |@Copyright (c) 2025 http://www.ladmin.cn All Rights Reserved.
// ------------------------------------------------------------------------
declare (strict_types=1);

namespace think\admin\service;

use think\admin\Service;

/**
 * JWT服务类
 * Class JwtService
 * @package think\admin\service
 */
class JwtService extends Service
{
    /**
     * 时间误差容忍度（秒）
     * 用于验证 nbf (Not Before), iat (Issued At), exp (Expiration Time) 声明时，
     * 允许当前时间和声明时间之间存在一定的误差，以处理服务器间轻微的时间不同步。
     * @var int
     */
    public static int $leeway = 0;

    public function initialize(): void
    {
        //1
    }

    /**
     * 支持的 HMAC 算法及其对应的 PHP 内建 hash_hmac 算法名称。
     * @var array<string, string>
     */
    private static array $supportedHmacAlgorithms = [
        'HS256' => 'sha256',
        'HS384' => 'sha384',
        'HS512' => 'sha512',
    ];

    /**
     * Base64URL 编码 (RFC 4648 Section 5)
     *
     * @param string $input 需要编码的原始字符串。
     * @return string Base64URL 编码后的字符串。
     */
    private function base64UrlEncode(string $input): string
    {
        return str_replace('=', '', strtr(base64_encode($input), '+/', '-_'));
    }

    /**
     * Base64URL 解码 (RFC 4648 Section 5)
     *
     * @param string $input 需要解码的 Base64URL 编码字符串。
     * @return string|false 解码后的原始字符串，如果输入无效则返回 false。
     */
    private function base64UrlDecode(string $input): string|false
    {
        $remainder = strlen($input) % 4;
        if ($remainder) {
            $padlen = 4 - $remainder;
            $input .= str_repeat('=', $padlen);
        }
        return base64_decode(strtr($input, '-_', '+/'));
    }

    /**
     * 安全的 JSON 编码
     *
     * @param mixed $data 需要编码的数据。
     * @return string JSON 格式的字符串。
     * @throws \JsonException 如果 JSON 编码过程中发生错误。
     */
    private function jsonEncode(mixed $data): string
    {
        $json = json_encode($data);
        if (json_last_error() !== JSON_ERROR_NONE) {
            // 抛出 JsonException，包含具体的错误信息
            throw new \JsonException('JSON encoding error: ' . json_last_error_msg());
        }
        return $json;
    }

    /**
     * 安全的 JSON 解码
     *
     * @param string $json 需要解码的 JSON 格式字符串。
     * @param bool $assoc 当为 true 时，返回关联数组；当为 false 时，返回对象 (默认为 true)。
     * @return mixed 解码后的 PHP 变量 (数组或对象)。
     * @throws \JsonException 如果 JSON 解码过程中发生错误。
     */
    private function jsonDecode(string $json, bool $assoc = true): mixed
    {
        $data = json_decode($json, $assoc);
        if (json_last_error() !== JSON_ERROR_NONE) {
            // 抛出 JsonException，包含具体的错误信息
            throw new \JsonException('JSON decoding error: ' . json_last_error_msg());
        }
        return $data;
    }

    /**
     * 生成 JWT 签名
     *
     * @param string $msg 要进行签名的消息 (通常是 `base64url(header).base64url(payload)`)。
     * @param string|\OpenSSLAsymmetricKey|resource $key 用于签名的密钥。
     *        对于 HMAC 算法，这是共享密钥字符串。
     *        对于 RSA/ECDSA 算法，这是私钥资源或 OpenSSLAsymmetricKey 对象。
     * @param string $alg 使用的签名算法 (例如 'HS256', 'RS256' 等)。
     * @return string 计算出的签名 (原始二进制格式)。
     * @throws \InvalidArgumentException 如果指定的算法不被支持。
     */
    private function sign(string $msg, $key, string $alg): string
    {
        // 检查是否为支持的 HMAC 算法
        if (array_key_exists($alg, self::$supportedHmacAlgorithms)) {
            $phpAlg = self::$supportedHmacAlgorithms[$alg];
            // 使用 PHP 内建 hash_hmac 函数生成签名
            return hash_hmac($phpAlg, $msg, $key, true);
        }

        // TODO: 在此添加对 RSA, ECDSA 等非对称算法的支持
        // 例如: 使用 openssl_sign()

        throw new \InvalidArgumentException('Algorithm [' . $alg . '] not supported');
    }

    /**
     * 将 Payload 数据编码为 JWT 字符串。
     *
     * @param array|object $payload 需要包含在 JWT 中的数据 (会进行 JSON 编码)。
     * @param string|\OpenSSLAsymmetricKey|resource $key 用于签名的密钥。
     * @param string $alg 使用的签名算法 (默认为 'HS256')。
     * @param array $head 额外的头部参数 (会与 `{'typ': 'JWT', 'alg': $alg}` 合并)。
     * @return string 生成的 JWT 字符串。
     * @throws \JsonException 如果 Header 或 Payload 的 JSON 编码失败。
     * @throws \InvalidArgumentException 如果签名算法不被支持。
     */
    public function encode($payload, $key, string $alg = 'HS256', array $head = []): string
    {
        // 合并默认头部和自定义头部
        $header = array_merge($head, ['typ' => 'JWT', 'alg' => $alg]);

        // 编码 Header 和 Payload
        $segments = [];
        $segments[] = $this->base64UrlEncode($this->jsonEncode($header));
        $segments[] = $this->base64UrlEncode($this->jsonEncode($payload));

        // 准备待签名的数据
        $signingInput = implode('.', $segments);

        // 计算签名
        $signature = $this->sign($signingInput, $key, $alg);
        $segments[] = $this->base64UrlEncode($signature);

        // 拼接成最终的 JWT 字符串
        return implode('.', $segments);
    }

    /**
     * 验证 JWT 签名是否有效。
     *
     * @param string $msg 用于验证签名的消息 (通常是 `base64url(header).base64url(payload)`)。
     * @param string $signature 从 JWT 中提取的签名 (原始二进制格式)。
     * @param string|\OpenSSLAsymmetricKey|resource $key 用于验证签名的密钥。
     *        对于 HMAC 算法，这是共享密钥字符串。
     *        对于 RSA/ECDSA 算法，这是公钥资源或 OpenSSLAsymmetricKey 对象。
     * @param string $alg 使用的签名算法 (例如 'HS256', 'RS256' 等)。
     * @return bool 如果签名有效则返回 true，否则返回 false。
     * @throws \InvalidArgumentException 如果指定的算法不被支持。
     */
    private function verify(string $msg, string $signature, $key, string $alg): bool
    {
        // 检查是否为支持的 HMAC 算法
        if (array_key_exists($alg, self::$supportedHmacAlgorithms)) {
            $phpAlg = self::$supportedHmacAlgorithms[$alg];
            // 重新计算预期签名
            $hash = hash_hmac($phpAlg, $msg, $key, true);
            // 使用 hash_equals 进行恒定时间比较，防止时序攻击
            return hash_equals($signature, $hash);
        }

        // TODO: 在此添加对 RSA, ECDSA 等非对称算法的验证支持
        // 例如: 使用 openssl_verify()

        throw new \InvalidArgumentException('Algorithm [' . $alg . '] not supported for verification');
    }

    /**
     * 解码 JWT 字符串，验证签名和标准时间声明。
     *
     * @param string $jwt 需要解码和验证的 JWT 字符串。
     * @param string|\OpenSSLAsymmetricKey|resource $key 用于验证签名的密钥。
     * @param array $allowedAlgs 允许的签名算法列表 (例如 `['HS256', 'HS384']`)。
     * @return object 解码后的 Payload 数据 (作为 stdClass 对象)。
     * @throws \InvalidArgumentException 如果 `$key` 为空或 `$allowedAlgs` 不是有效的非空数组。
     * @throws \think\Exception JWT 核心验证失败:
     *         - code 10001: 签名无效 (Signature verification failed)。
     *         - code 10002: 令牌尚未生效 (Cannot handle token prior to [nbf time])。
     *         - code 10003: 令牌已过期 (Expired token)。
     * @throws \UnexpectedValueException JWT 格式错误、算法不允许、iat 在未来等其他验证错误。
     * @throws \JsonException Header 或 Payload 的 JSON 解码失败。
     */
    public function decode(string $jwt, $key, array $allowedAlgs = ['HS256']): object
    {
        $timestamp = time(); // 获取当前时间戳，用于声明验证

        // 基础参数校验
        if (empty($key)) {
            throw new \InvalidArgumentException('Key may not be empty');
        }
        if (!is_array($allowedAlgs) || empty($allowedAlgs)) {
            throw new \InvalidArgumentException('Allowed algorithms must be a non-empty array');
        }

        // 检查 JWT 结构
        $parts = explode('.', $jwt);
        if (count($parts) !== 3) {
            throw new \UnexpectedValueException('Wrong number of segments in JWT');
        }

        list($headb64, $bodyb64, $sigb64) = $parts;

        // 解码 Header
        $headerRaw = $this->base64UrlDecode($headb64);
        if (false === $headerRaw) {
            throw new \UnexpectedValueException('Invalid encoding in JWT Header');
        }
        $header = $this->jsonDecode($headerRaw, false);
        if (null === $header) {
            throw new \UnexpectedValueException('Invalid JSON in JWT Header');
        }
        // 解码 Payload
        $payloadRaw = $this->base64UrlDecode($bodyb64);
        if (false === $payloadRaw) {
            throw new \UnexpectedValueException('Invalid encoding in JWT Payload');
        }
        $payload = $this->jsonDecode($payloadRaw, false); // 获取 stdClass 对象
        if (null === $payload) {
            throw new \UnexpectedValueException('Invalid JSON in JWT Payload');
        }

        // 解码签名
        $signature = $this->base64UrlDecode($sigb64);
        if (false === $signature) {
            throw new \UnexpectedValueException('Invalid encoding in JWT Signature');
        }

        // 检查算法声明
        if (!isset($header->alg)) {
            throw new \UnexpectedValueException('Algorithm (alg) not specified in JWT Header');
        }
        if (!in_array($header->alg, $allowedAlgs, true)) {
            throw new \UnexpectedValueException('Algorithm [' . $header->alg . '] is not allowed');
        }

        // 验证签名 (最关键的安全步骤)
        if (!$this->verify("$headb64.$bodyb64", $signature, $key, $header->alg)) {
            // 签名验证失败，抛出带特定错误码的 think\Exception
            throw new \think\Exception('Signature verification failed', 10001);
        }

        // 验证 nbf (Not Before) 声明
        if (isset($payload->nbf)) {
            if (!is_numeric($payload->nbf)) {
                throw new \UnexpectedValueException('Invalid Not Before (nbf) claim format');
            }
            if ($payload->nbf > ($timestamp + self::$leeway)) {
                // 令牌尚未到生效时间，抛出带特定错误码的 think\Exception
                throw new \think\Exception(
                    'Cannot handle token prior to ' . date(\DateTimeInterface::ISO8601, $payload->nbf),
                    10002
                );
            }
        }

        // 验证 iat (Issued At) 声明
        if (isset($payload->iat)) {
            if (!is_numeric($payload->iat)) {
                throw new \UnexpectedValueException('Invalid Issued At (iat) claim format');
            }
            // iat 声明必须在当前时间之前（或在误差范围内）
            if ($payload->iat > ($timestamp + self::$leeway)) {
                throw new \UnexpectedValueException(
                    'Issued At (iat) claim cannot be in the future'
                );
            }
        }

        // 验证 exp (Expiration Time) 声明
        if (isset($payload->exp)) {
            if (!is_numeric($payload->exp)) {
                throw new \UnexpectedValueException('Invalid Expiration Time (exp) claim format');
            }
            // 令牌已过期（当前时间 >= 过期时间 - 误差）
            if (($timestamp - self::$leeway) >= $payload->exp) {
                // 令牌已过期，抛出带特定错误码的 think\Exception
                throw new \think\Exception('Expired token', 10003);
            }
        }

        // 所有验证通过，返回解码后的 Payload
        return $payload;
    }
}
